refactor(app-router): extract app page dispatch#986
refactor(app-router): extract app page dispatch#986james-elicx merged 2 commits intocloudflare:mainfrom
Conversation
commit: |
7832b64 to
1a713e6
Compare
1a713e6 to
2122f64
Compare
2122f64 to
34924aa
Compare
|
/bigbonk review |
There was a problem hiding this comment.
Clean extraction that follows the stated architecture principle: generated entries describe app shape, normal modules own behavior.
What I checked
- Behavioral fidelity: The dispatch module reproduces the same pipeline order as the old inline code: fetch soft tags → default-export check → method policy → force-static/dynamic-error context setup → ISR cache read → dynamic params validation → intercept resolution → page element build → render lifecycle. No steps reordered or dropped.
setCurrentFetchSoftTagstiming improvement: The old entry calledsetCurrentFetchSoftTagswith a ternary (route.routeHandler ? "route" : "page") before the route-handler/page branch. The new code correctly tags route handlers with"route"in the entry (line 1125) and pages with"page"in dispatch (line 282). Same net behavior but cleaner separation.- Import resolution: All imports in
app-page-dispatch.tsresolve to existing modules.createAppPageTreePathis exported fromapp-page-route-wiring.tsx:153.createRequestContext/runWithRequestContextin the dispatch module are the same functions the entry previously aliased as_createUnifiedCtx/_runWithUnifiedCtx. - Type safety at the seam: The
DispatchAppPageOptionstype is well-structured — route shape is constrained byTRoute extends AppPageDispatchRoute, all callbacks are properly typed, and optional fields (debugClassification,isrDebug,scriptNonce,mountedSlotsHeader) correctly use?. - Test coverage: The ~170 deleted test lines in
app-router.test.tswere generated-code string assertions (checking that__readAppPageCacheResponse,__validateAppPageDynamicParams,__renderAppPageLifecycleappeared in the template). These are replaced by: (1) the newentry-templates.test.tsassertions verifying the delegation contract (dispatchAppPage as __dispatchAppPage,return __dispatchAppPage({), and (2) the newapp-page-dispatch.test.tswith response-level behavioral tests. No behavioral coverage lost. - CI: All checks green — Check, Vitest (unit + 3 integration shards), all 10 E2E projects.
Minor observations (non-blocking)
Two small things noted inline. Neither blocks merge.
| extraTags: string[], | ||
| routeSegments: readonly string[], | ||
| ): string[] { | ||
| return buildPageCacheTags(cleanPathname, extraTags, [...routeSegments], "page"); |
There was a problem hiding this comment.
Nit: buildPageCacheTags takes routeSegments: string[] (mutable array), but AppPageDispatchRoute.routeSegments is readonly string[]. The spread [...routeSegments] here works correctly to satisfy the type, but createAppPageTreePath on line 531 does the same spread. If buildPageCacheTags and createAppPageTreePath accepted readonly string[] instead, these defensive copies wouldn't be needed. Not blocking — just noting the pattern for a future cleanup pass.
| import { buildPageCacheTags } from "./implicit-tags.js"; | ||
| import type { ISRCacheEntry } from "./isr-cache.js"; | ||
|
|
||
| type AppPageParams = Record<string, string | string[]>; |
There was a problem hiding this comment.
Several type aliases here (AppPageParams, AppPageBoundaryOnError, AppPageDebugLogger, AppPageCacheSetter, AppPageCacheGetter) are duplicated from app-page-render.ts (lines 35-46). These could be shared from a common types module to avoid drift. Non-blocking since they're simple types unlikely to diverge, but worth noting for a future types consolidation pass.
|
Review submitted with APPROVE. Here's a summary of what I found: Overall: This is a well-executed extraction. The ~434-line inline page dispatch pipeline in the generated RSC entry is now a properly typed 625-line module at Key verification points:
Two non-blocking observations left as inline comments:
|
App Router page requests now enter a shared dispatch module after generated route matching. The generated RSC entry still describes route modules, app-shape callbacks, and request-local values, but cache reads, static generation context, dynamic param checks, intercept responses, and lifecycle rendering move into normal typed code. This keeps page orchestration out of codegen without changing the observable App Router behavior covered by the integration suite.
34924aa to
888ca29
Compare
What changed
This extracts App Router page request orchestration out of the generated RSC entry and into
packages/vinext/src/server/app-page-dispatch.ts.The generated entry now describes app shape and request wiring only: matched route, params, component tree builder, boundary render callbacks, middleware context, cache keys, and module loaders. The normal runtime module owns behavior: method policy, force-static/error setup, ISR cache read/regeneration, dynamic params validation, intercepting route responses, page element build error handling, lifecycle render delegation, and special-error fallback routing.
Why
This follows the principle that codegen should describe the app shape while normal modules implement behavior. The prior generated template mixed route-specific imports with a large page dispatch pipeline, which made orchestration harder to type, test, review, and evolve independently from route manifest generation.
Behavior tests
Added
tests/app-page-dispatch.test.tswith response-level behavior coverage for the new dispatch module:dynamicParams = falsereturns 404 for paths outside generated paramsThese tests assert HTTP-visible outcomes: status, headers, and response body. They avoid asserting dispatch call counts or generated implementation details.
Next.js references
Next.js uses a similar separation between generated app-page shape and runtime page rendering modules:
AppPageRouteModuleAppPageRouteModuleis the runtime module boundary for app page renderingAppPageRouteModule.render()delegates torenderToHTMLOrFlightrenderToHTMLOrFlightImplowns the app render orchestrationrenderToHTMLOrFlightexposes the stable runtime entrypointValidation
vp check --fix knip.ts packages/vinext/src/entries/app-rsc-entry.ts packages/vinext/src/server/app-page-dispatch.ts packages/vinext/src/server/app-page-request.ts tests/app-page-dispatch.test.ts tests/app-router.test.ts tests/entry-templates.test.ts tests/__snapshots__/entry-templates.test.ts.snapvp test run tests/app-page-dispatch.test.tsvp test run tests/app-page-dispatch.test.ts tests/entry-templates.test.ts tests/app-router.test.ts tests/app-page-render.test.ts tests/app-page-request.test.ts tests/app-page-cache.test.tsvp run knip --no-progressRisk coverage
Covered the main behavioral risks through dedicated dispatch behavior tests plus the App Router integration suite: dev/prod App Router rendering, RSC/HTML responses, ISR cache reads and writes, route handler separation, middleware header propagation, dynamic params validation, intercepting routes, not-found/forbidden/unauthorized boundaries, and generated-entry delegation assertions.
The generated-code tests intentionally assert only the delegation contract now. Runtime dispatch behavior is covered by
tests/app-page-dispatch.test.ts, existing helper tests, and the App Router integration suite.